"""
Cpm/cpm: Описание пакетного менеджера. Управление списками пакетов.
Author: Milinuri Nirvalen
Ver: 2.0
"""

from console import Console
from config import Config, save_file, load_file
from index import PackagesIndex
from screen import Logger
from utils import print_dict, check_params, package_base, ask_user

import time
import os
import requests

from icecream import ic


log = Logger('Cpm')


def load_file_from_url(session, url):
    """Загружает файл по ссылке.
    
    :param session: requests сессия
    :param url: ссылка для загрузки файла"""

    try:
        r = session.get(url)

        if r.status_code != 200:
            log.warn(f'{r.status_code}: {url}')
            return {}

        log.info(f'L: {url}')
        return r.text
    
    except Exception as e:
        log.error(f'{url}: {e}')
        return {}

def load_files(files):
    """Загружает файлы из словаря {path:url}

    :param files: Словарь {path: url} для загрузки файлов"""

    with requests.Session() as s:
        for k, v in files.items():
            file = load_file_from_url(s, v)
            p = k.rsplit('/', 1)[0]

            if file:
                if not os.path.exists(p):
                    os.makedirs(p)

                with open(k, 'w') as f:
                    f.write(file)


class SearchEngine:
    """docstring for SearchPackages"""

    def __init__(self, index):
        super(SearchEngine, self).__init__()
        self.index = index

        self.groups = {}
        self.response = {}

    def add_in_group(self, pid, name, key, value):
        """Сравнрвает параметры пакета с поисковым запросом.
        Добавляет резульат в self.groups если есть совпадение.

        :param data: словарь с текущими результатами поиска
        :param pid: package ID для которого ведётся поиск
        :param name: имя группы поиска
        :param key: строка, для которой применяется поиск
        :param value: ввод от пользователя
        """

        if not isinstance(key, str):
            key = str(key)

        if value.lower() in key.lower():
            if not pid in self.groups:
                self.groups[pid] = [name]
            else:
                self.groups[pid].append(name)

    def gen_response(self):
        """Группирует значения из self.groups в self.response"""

        for k, v in self.groups.items():
            g = ", ".join(v)

            if g not in self.response:
                self.response[g] = [k]
            else:
                self.response[g].append(k)

    def get_groups(self, args):
        """Собирает данные о пакетах их self.index в self.groups.

        :param args: строка поиска по пакетам в индексе"""

        for k, v in self.index.data.items():
            self.add_in_group(pid=k, name='pid', key=k, value=args)

            for n, t in v.items():
                if isinstance(t, list):
                    for x in t:
                        self.add_in_group(pid=k, name=n, key=x, value=args)
                    continue

                self.add_in_group(pid=k, name=n, key=t, value=args)

    def search(self, args):
        """ищет пакеты по их параметрам и возвращает PID

        :param args: строка для поиска"""

        self.get_groups(args)
        self.gen_response()

        for k, v in sorted(self.response.items(), key=lambda x: len(x[1])):
            print(f'\n{k}')
            self.index.packages_list(v)


class PackagesManager(Console):
    """docstring for PackagesManager"""

    def __init__(self):
        super(PackagesManager, self).__init__()
        self.config = Config().read()
        self.rpath = "cpm_data/repositories/"

        # определение команд консольного модуля
        # =====================================

        self.add_command(self.locate_packages, 'locate',
                         usage= 'Собирает локальный индекс пакетов')

        self.add_command(self.remove_packages, 'remove',
                         usage= '[targets] Удаляет пакеты из системы')

        self.add_command(self.packages_info, 'package',
                         usage= '[targets] Информация о пакетах',
                         flags= {"global": False})

        self.add_command(self.packages_status, 'list',
                         usage= 'Cтатус пакетов',
                         flags= {'status': False, "global":False})

        self.add_command(self.index_info, 'info',
                         usage= 'Информация о настройках и индексах CPM',
                         flags= {"global":False})

        """
        self.add_command(self.search_packages, 'search',
                         usage='[-g pattern] ищет пакеты по параметрам в индексе')
        """

        self.add_command(self.compile_global_index, 'update',
                         usage= 'Собирает глобальный индекс пакетов',
                         flags= {"force": False})

        self.add_command(self.install_packages, 'install',
                         usage="[targets] Устанавливает пакеты",
                         flags= {"force": False})

        self.add_command(self.get_upgradable_packages, 'upgrade',
                         usage='Проверяет обновления для пакетов')

    def select_index(self, ctx):
        """Выбирает индекс для тех команд, которые это поддерживают.
        Если в команде указан флаг "global", выдаётся глобальный индекс.

        :param ctx: Экземпляр Context

        :return: PackageIndex локальный или глобальный"""

        if ctx.get_flag('global'):
            return PackagesIndex(load_file("global_index.toml"), self.config)
        
        return PackagesIndex(load_file("index.toml"), self.config)


    # команды для локального индекса пакетов
    # ======================================

    def locate_packages(self, ctx):
        """[Команда] Собирает пакетный индекс.
        Ищет локальные пакеты в системе.
        Также проверяет различные проблемы с пакетами."""

        p = PackagesIndex({}, self.config)
        p.compile()
        save_file("index.toml", p.data)

    def remove_packages(self, ctx):
        """[Команда] Удаляет локальные пакетв из системы.
        Если пакет требуется другим пакетам, вы не сможете его удалить.
        """
        
        log.group(f"Удаление {ctx.largs}...")
        index = PackagesIndex(load_file("index.toml"), self.config)
        resp = index.remove_packages(ctx.args)

        if resp:
            self.locate_packages(ctx)

    # команды для смешанного индекса пакетов
    # ======================================

    def packages_info(self, ctx):
        """[Команда] Отображает информацию о пакетах из индекса."""

        log.group(f'Информацию о пакетах: {ctx.largs}')
        index = self.select_index(ctx)
        for pid in index.get_packages_by_pid(ctx.args):
            index.get_package(pid).print_params()

    def packages_status(self, ctx):
        """[Команда] Отображает статусы пакетов в индексе."""
        index = self.select_index(ctx)

        if ctx.get_flag("status"):
            index.packages_list(mini=False, desc=False)
        else:
            index.packages_list()

    def index_info(self, ctx):
        """[Команда] Информацию о настройках cpm и индексах пакетов."""
        
        index = self.select_index(ctx)

        log.group("Параметры пакетов по умолчанию:")
        print_dict(self.config["package_base"])
        log.group("Реопзитории:")
        
        for k, v in self.config["repositories"].items():
            log.info(f"{k}= {v['url']}")

        packages_types = {'all': len(index.data)}
        for k, v in index.data.items():
            packages_types[v['type']] = packages_types.get(v['type'], 0) + 1

        log.group("Типы пакетов:")
        p_types = ''
        for k, v in sorted(index.count_types().items()):
            p_types += f' {v} \033[34m{k}\033[0m; '

        print(p_types)

    '''
    def search_packages(self, ctx):
        """Писк пакетов в индексе по параметрам."""
        log.group(f'Поиск {ctx.sargs}')
        index = self.select_index(ctx)
        s = SearchEngine(index)
        s.search(ctx.sargs)
    '''

    # команды для глобального индекса пакетов
    # =======================================

    def update_global_index(self, ctx):
        """Обновляет глобальные индексы пакетов."""

        log.group('Обновление реопзиториев...')
        now = int(time.time())
        load = {}
        edited = False

        for k, v in self.config["repositories"].items():
            if v["updated"]+(3600*20) < now or ctx.get_flag("force"):
                load[self.rpath+k +
                     '.toml'] = os.path.join(v['url'], "index.toml")
                v['updated'] = now
                edited = True

            self.config['repositories'][k] = v

        if edited:
            save_file('cpm_data/repositories.toml',
                      self.config['repositories'])

        if not load:
            log.complite(f"Все реопзиторие обновлены")
            return True

        load_files(load)

    def compile_global_index(self, ctx):
        """[Команда] Собирает глобальный индекс из репозиториев."""
        
        self.update_global_index(ctx)
        log.group('Сборка глобального индекса пакетов...')
        global_index = {}

        for k, v in self.config['repositories'].items():
            res = load_file(os.path.join(self.rpath, k+'.toml'))
            index = PackagesIndex(res, self.config)

            for p, d in index.data.items():
                d = check_params(d, package_base)
                requirements = []

                for x in d['require']:
                    if isinstance(x, list):
                        res = []
                        for xx in x:
                            if xx.count('.'):
                                res.append(xx)
                            else:
                                res.append(k+'.'+xx)
                        requirements.append(res)
                        continue

                    if x.count('.'):
                        requirements.append(x)
                    else:
                        requirements.append(k+'.'+x)

                d['require'] = requirements
                d['url'] = v['url']
                global_index[k+'.'+p] = d

        save_file("global_index.toml", global_index)

    def load_packages(self, packages, index, gindex):
        """Загружает пакеты по PID из глобального индекса.
        
        :param packages: Cписок PID лля загрузки
        :param index: Локальеый индекс пакетов
        :param gindex: Глобальный индекс пакетов"""
        
        load = {}

        for pid in packages:
            p = index.get_package(pid.split('.', 1)[1])
            gp = gindex.get_package(pid)
            url = gp.data.get('url')

            if not url:
                log.error(f'{k}: Отцуцтвует ссылка для загрузки файлов')
                continue

            log.info(f'Загрузка {pid} -> {gp.path}',)

            for k, v in gp.files.items():
                k = k.strip('/')
                name = k.rsplit('/', 1)[-1]
                path = gp.path+"/"+name

                if name == "packages.toml":
                    save_file(path, {pid.split('.', 1)[1]: gindex.clear_package(pid)})
                    log.complite(f'{pid} packages.toml')
                    continue

                if v == p.get_file_hash(path):
                    log.compile(f'Pass {path}')
                    continue

                load[path] = os.path.join(url, k)

        load_files(load)

    def install_packages(self, ctx):
        """Загружает пакеты из глобального индекса
        :param args: Список PID для загрузки"""

        log.group(f'Установка пакетов {ctx.largs}...')

        # получение паектов и зависимостей
        index = PackagesIndex(load_file("index.toml"), self.config)
        gindex = PackagesIndex(load_file("global_index.toml"), self.config)
        r = gindex.get_require(gindex.get_packages_by_pid(ctx))

        # отсекаем пакеты, совпадающие номером сборки
        for pid in list(r):
            p = index.get_package(pid.split('.', 1)[1])
            gp = gindex.get_package(pid)

            if p.build >= gp.build and gp.build > 1:
                if not ctx.get_flag("force"):
                    log.complite(f'Pass {pid}')
                    r.remove(pid)

        if not r:
            return True
    
        gindex.packages_list(r)
        
        if ask_user():
            self.load_packages(r, index, gindex)
            self.locate_packages(ctx)

    def get_upgradable_packages(self, ctx):
        """Проверяет обновления для локальных пакетов. 
        C последующим обновлением."""
        
        log.group('Проверка обновлений.')
        upgradeble = []
        index = PackagesIndex(load_file("index.toml"), self.config)
        gindex = PackagesIndex(load_file("global_index.toml"), self.config)

        # проходимся по локальным пакетам и смотрим что можно обновить
        for k, v in index.data.items():
            pid = gindex.get_packages_by_pid([k], optional=False)

            if not len(pid):
                continue

            if pid[0] in gindex.data:
                p = gindex.data[pid[0]]

                if v["build"] < p["build"]:
                    log.info(f'{k} V{v["ver"]} ({v["build"]}) -> V{p["ver"]} ({p["build"]})')
                    upgradeble.append(pid[0])

                elif v["build"] > p["build"]:
                    log.info(f'{k} V{v["ver"]} ({v["build"]}) <- {p["ver"]} ({p["build"]})')

        if not upgradeble:
            log.complite('Все пакеты обновлены')
            return True

        if ask_user():
            self.load_packages(upgradeble, index, gindex)
            self.locate_packages(ctx)
